<!doctype html>
<html>
	<head>
		<style>
			body {
				--bg-1: hsl(0, 0%, 100%);
				--bg-2: hsl(206, 20%, 90%);
				--bg-3: hsl(206, 20%, 80%);
				--fg-1: hsl(0, 0%, 13%);
				--fg-2: hsl(0, 0%, 20%);
				--fg-2: hsl(0, 0%, 30%);
				--link: hsl(208, 77%, 47%);
				--link-hover: hsl(208, 77%, 55%);
				--link-active: hsl(208, 77%, 40%);
				--border-radius: 4px;
				--font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
					'Open Sans', 'Helvetica Neue', sans-serif;
				--font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas,
					'DejaVu Sans Mono', monospace;
				color-scheme: light;
				background: var(--bg-1);
				color: var(--fg-1);
				font-family: var(--font);
				line-height: 1.5;
				margin: 1rem;
				height: calc(100vh - 2rem);
				accent-color: var(--hover) !important;
			}

			a {
				color: var(--link);
			}

			a:hover {
				color: var(--link-hover);
			}

			a:active {
				color: var(--link-active);
			}

			code {
				background: var(--bg-2);
				font-family: var(--font-mono);
				font-size: 0.9em;
				padding: 0.15rem 0.3rem;
				border-radius: var(--border-radius);
			}

			ul.todos {
				padding: 0;
			}

			body.dark {
				color-scheme: dark;
				--bg-1: hsl(0, 0%, 18%);
				--bg-2: hsl(0, 0%, 30%);
				--bg-3: hsl(0, 0%, 40%);
				--fg-1: hsl(0, 0%, 90%);
				--fg-2: hsl(0, 0%, 70%);
				--fg-3: hsl(0, 0%, 60%);
				--link: hsl(206, 96%, 72%);
				--link-hover: hsl(206, 96%, 78%);
				--link-active: hsl(206, 96%, 64%);
			}
		</style>

		<script>
			(function () {
				function send(payload, origin = '*') {
					parent.postMessage(payload, origin);
				}

				window.addEventListener('message', ({ origin, data }) => {
					let { action, cmd_id } = data;

					const reply = (payload) => send({ ...payload, cmd_id }, origin);

					try {
						if (action === 'set_theme') {
							document.body.classList.toggle('dark', data.args.theme === 'dark');
						}

						if (action === 'eval') {
							(0, eval)(data.args.script);
						}

						if (action === 'catch_clicks') {
							// attached to document to not interfere with svelte's event delegators that are attached to the app root (document.body)
							document.addEventListener('click', (event) => {
								if (event.which !== 1) return;
								if (event.metaKey || event.ctrlKey || event.shiftKey) return;
								if (event.defaultPrevented) return;

								// ensure target is a link
								let el = event.target;
								while (el && el.nodeName !== 'A') el = el.parentNode;
								if (!el || el.nodeName !== 'A') return;

								if (
									el.hasAttribute('download') ||
									el.getAttribute('rel') === 'external' ||
									el.target
								)
									return;

								event.preventDefault();

								if (el.href.startsWith(origin)) {
									const url = new URL(el.href);
									if (url.hash[0] === '#') {
										window.location.hash = url.hash;
										return;
									}
								}

								window.open(el.href, '_blank');
							});
						}

						reply({ action: 'cmd_ok' });
					} catch ({ message, stack }) {
						reply({ action: 'cmd_error', message, stack });
					}
				});

				window.onerror = function (msg, url, lineNo, columnNo, error) {
					send({ action: 'error', value: error });
				};

				window.addEventListener('unhandledrejection', (event) => {
					send({ action: 'unhandledrejection', value: event.reason });
				});

				// Intercept console methods
				const timers = new Map();
				const counters = new Map();

				function log(command, opts) {
					try {
						send({ action: 'console', command, ...opts });
					} catch {
						send({ action: 'console', command: 'unclonable' });
					}
				}

				function stringify(args) {
					try {
						return JSON.stringify(args, (key, value) => {
							// if we don't do this, our Set/Map from svelte/reactivity would show up wrong in the console
							if (value instanceof Map) {
								return { type: 'Map', value };
							}

							if (value instanceof Set) {
								return { type: 'Set', value };
							}

							return value;
						});
					} catch (error) {
						return null;
					}
				}

				/** @param {string} method */
				function stack(method) {
					return new Error().stack
						.split('\n')
						.filter((line) => {
							if (/[(@]about:srcdoc/.test(line)) return false;
							return true;
						})
						.slice(1)
						.map((line) => {
							line = line
								.replace('console[method]', `console.${method}`)
								.replace(/console\.<computed> \[as \w+\]/, `console.${method}`);

							let match =
								/^\s+at (.+) \((.+:\d+:\d+)\)/.exec(line) || /^(.+)@(.+:\d+:\d+)?/.exec(line);

							if (match) {
								return {
									label: match[1],
									location: match[2]
								};
							}

							return null;
						})
						.filter((x) => x);
				}

				const can_dedupe = ['log', 'info', 'dir', 'warn', 'error', 'assert', 'trace'];

				const methods = {
					clear: () => log('clear'),
					// TODO make the command 'push' and the level/type 'info'
					log: (...args) => log('info', { args }),
					info: (...args) => log('info', { args }),
					dir: (...args) => log('info', { args: [args[0]], expanded: true }),
					warn: (...args) => log('warn', { args, stack: stack('warn'), collapsed: true }),
					error: (...args) => log('error', { args, stack: stack('error'), collapsed: true }),
					assert: (condition, ...args) => {
						if (condition) return;
						log('error', {
							args: ['Assertion failed:', ...args],
							stack: stack('assert'),
							collapsed: true
						});
					},
					group: (...args) => log('group', { args, collapsed: false }),
					groupCollapsed: (...args) => log('group', { args, collapsed: true }),
					groupEnd: () => log('groupEnd'),
					table: (...args) => {
						const data = args[0];
						if (data && typeof data === 'object') {
							log('table', { data, columns: args[1] });
						} else {
							log('info', { args });
						}
					},
					time: (label = 'default') => timers.set(label, performance.now()),
					timeLog: (label = 'default') => {
						const now = performance.now();
						if (timers.has(label)) {
							log('info', { args: [`${label}: ${now - timers.get(label)}ms`] });
						} else {
							log('warn', { args: [`Timer '${label}' does not exist`] });
						}
					},
					timeEnd: (label = 'default') => {
						const now = performance.now();
						if (timers.has(label)) {
							log('info', { args: [`${label}: ${now - timers.get(label)}ms`] });
						} else {
							log('warn', { args: [`Timer '${label}' does not exist`] });
						}
						timers.delete(label);
					},
					count: (label = 'default') => {
						counters.set(label, (counters.get(label) || 0) + 1);
						log('info', { args: [`${label}: ${counters.get(label)}`] });
					},
					countReset: (label = 'default') => {
						if (counters.has(label)) {
							counters.set(label, 0);
						} else {
							log('warn', { args: [`Count for '${label}' does not exist`] });
						}
					},
					trace: (...args) => {
						log('info', {
							args: args.length === 0 ? ['console.trace'] : args,
							stack: stack('trace'),
							collapsed: false
						});
					}
				};

				let previous = '';

				for (const method in methods) {
					const original = console[method];

					console[method] = (...args) => {
						const stack = new Error().stack;

						if (
							previous === (previous = stringify({ method, args, stack })) &&
							can_dedupe.includes(method) &&
							args.every((arg) => !arg || typeof arg !== 'object')
						) {
							send({ action: 'console', command: 'duplicate' });
						} else {
							methods[method](...args);
						}

						original(...args);
					};
				}
			})();
		</script>
	</head>
	<body></body>
</html>
